﻿namespace Microsoft.Communications.Contacts
{
    using System;
    using System.Collections.Generic;
    using System.IO;
    using System.Linq;
    using System.Text;
    using Standard;

    /// <summary>
    /// A view that exposes Windows Contacts Group properties on a Contact
    /// </summary>
    /// <remarks>
    /// </remarks>
    public class MapiGroupView : ContactView
    {
        private const string _ContactIdKey = "[MSWABMAPI]PropTag0x66001102";
        private const string _NameEmailKey = "[MSWABMAPI]PropTag0x80091102";
        private const string _MapiPrefix = "CID_V1:";
        
        // One-off members contain some unused prefix data.
        // It's of constant size and can be ignored.
        private static readonly byte[] _OneOffPrefixJunk = new byte[]
        {
            0, 0, 0, 0, 129, 
            43, 31, 164, 190, 163, 
            16, 25, 157, 110, 0, 
            221, 1, 15, 84, 2, 
            0, 0, 1, 128
        };
        private static readonly byte[] _MapiPropertyTerminator = new byte[] {0, 0};

        private readonly UnicodeEncoding _Encoding = new UnicodeEncoding();

        private readonly List<string> _contactIds;
        private readonly List<Person> _oneOffs;

        /// <summary>
        /// Check whether a Contact contains properties for the paths of MAPI Groups.
        /// </summary>
        /// <param name="contact">The contact to check for MAPI-groupness</param>
        /// <returns>
        /// Returns true if the contact would have values for either MemberIds or
        /// MemberNameEmailPairs if the contact was viewed as a MAPI group.  False
        /// otherwise.
        /// </returns>
        /// <remarks>
        /// This does not check that the properties are valid, just that the property
        /// paths of MAPI Group properties exist in the given contact.
        /// </remarks>
        public static bool HasMapiProperties(Contact contact)
        {
            Verify.IsNotNull(contact, "contact");

            using (Stream stm = contact.GetBinaryProperty(_ContactIdKey, null))
            {
                if (null != stm)
                {
                    return true;
                }
            }

            using (Stream stm = contact.GetBinaryProperty(_NameEmailKey, null))
            {
                if (null != stm)
                {
                    return true;
                }
            }

            return false;
        }

        /// <summary>
        /// Get a view over the contact that accesses Windows Contacts group properties.
        /// </summary>
        /// <param name="contact">The contact to create the group-view over.</param>
        /// <remarks>
        /// This does not create an actively updated view over the contact's properties.
        /// Changes to the contact in memory or on disk are not reflected by this instance.
        /// </remarks>
        public MapiGroupView(Contact contact)
            : base(contact)
        {
            _oneOffs = _GetOneOffs(contact) ?? new List<Person>();
            _contactIds = _GetContactIds(contact) ?? new List<string>();
        }

        private List<Person> _GetOneOffs(Contact contact)
        {
            using (Stream stm = contact.GetBinaryProperty(_NameEmailKey, null))
            {
                if (null != stm)
                {
                    using (var binreader = new BinaryReader(stm))
                    {
                        var oneOffs = new List<Person>();
                        try
                        {
                            int pairCount = binreader.ReadInt32();
                            for (int i = 0; i < pairCount; ++i)
                            {
                                int cb = binreader.ReadInt32();
                                byte[] id = binreader.ReadBytes(cb);
                                // Seeing a double-null termination.  Trim it...
                                Assert.ArraysAreEqual(_MapiPropertyTerminator, new [] {id[id.Length-2], id[id.Length-1]});
                                string mapiId = _Encoding.GetString(id, _OneOffPrefixJunk.Length, id.Length - _MapiPropertyTerminator.Length - _OneOffPrefixJunk.Length);
                                // Should contain three elements: Name, e-mail type, e-mail address.
                                string[] tokens = mapiId.Split(new[] {'\0'}, StringSplitOptions.None);
                                if (3 != tokens.Length)
                                {
                                    throw new FormatException("Group contains invalid one-off member data.");
                                }
                                // Throw away the e-mail address type.
                                oneOffs.Add(new Person(tokens[0], null, tokens[2], null));
                            }
                        }
                        catch (EndOfStreamException)
                        {
                            throw new FormatException("Group contains invalid member data.  The data stream ended prematurely.");
                        }
                        if (stm.Position != stm.Length)
                        {
                            throw new FormatException(
                                "Group contains invalid member data.  More data was found than was specified in the header.");
                        }

                        return oneOffs;
                    }
                }
            }
            return null;
        }

        private List<string> _GetContactIds(Contact contact)
        {
            using (Stream stm = contact.GetBinaryProperty(_ContactIdKey, null))
            {
                if (null != stm)
                {
                    using (var binreader = new BinaryReader(stm))
                    {
                        var contactIds = new List<string>();

                        try
                        {
                            var idCount = binreader.ReadInt32();
                            for (int i = 0; i < idCount; ++i)
                            {
                                int cb = binreader.ReadInt32();
                                byte[] id = binreader.ReadBytes(cb);
                                // Seeing a double-null termination.  Trim it...
                                Assert.ArraysAreEqual(new byte[] {0, 0}, new[] {id[id.Length - 2], id[id.Length - 1]});
                                string mapiId = _Encoding.GetString(id, 0, id.Length - 2);
                                if (!mapiId.StartsWith(_MapiPrefix, StringComparison.Ordinal))
                                {
                                    throw new FormatException(
                                        "Group contains invalid member data.  ContactIds don't match the expected format.");
                                }
                                contactIds.Add(mapiId.Substring(_MapiPrefix.Length));
                            }
                        }
                        catch (EndOfStreamException)
                        {
                            throw new FormatException("Group contains invalid member data.  The data stream ended prematurely.");
                        }
                        if (stm.Position != stm.Length)
                        {
                            throw new FormatException(
                                "Group contains invalid member data.  More data was found than was specified in the header.");
                        }

                        return contactIds;
                    }
                }
            }
            return null;
        }

        /// <summary>
        /// Get the collection of ContactIds associated as members with this MAPI group.
        /// Because this is not a normal Contact property, the returned collection can not be modified to change the group membership.
        /// To modify the group membership, call ReplaceMemberIds.
        /// </summary>
        public IList<string> MemberIds
        {
            get
            {
                return _contactIds.AsReadOnly(); 
            }
        }

        /// <summary>
        /// Get the collection of Name/Email pairs associated as members with this MAPI group.
        /// Because this is not a normal Contact property, the returned collection can not be modified to change the group membership.
        /// To modify the group membership, call ReplaceOneOffMembers.
        /// </summary>
        public IList<Person> OneOffMembers
        {
            get
            {
                return _oneOffs.AsReadOnly();
            }
        }

        public void ReplaceMemberIds(IEnumerable<string> memberIds)
        {
            _contactIds.Clear();

            if (memberIds == null)
            {
                Source.RemoveProperty(_ContactIdKey);
                return;
            }

            var contactIdList = memberIds as IList<string> ?? memberIds.ToList();

            using (Stream stm = _GenerateMemberIds(contactIdList))
            {
                if (stm == null)
                {
                    Source.RemoveProperty(_ContactIdKey);
                }
                else
                {
                    Source.SetBinaryProperty(_ContactIdKey, stm, "binary/x-ms-wab-mapi");
                    _contactIds.AddRange(contactIdList);
                }
            }
        }

        private Stream _GenerateMemberIds(IList<string> memberIds)
        {
            if (memberIds.Count == 0)
            {
                return null;
            }

            var stm = new MemoryStream();
            try
            {
                var binwriter = new BinaryWriter(stm, _Encoding);

                // The first item is the number of members that are going to be written.
                binwriter.Write(memberIds.Count);

                // Create a string "CID_V1:<id>" and encode as unicode.
                foreach (byte[] encodedId in memberIds.Select(memberId => _Encoding.GetBytes(_MapiPrefix + memberId)))
                {
                    // We'll write out the length in bytes of the encoded property and then the encoded string into the property.
                    binwriter.Write(encodedId.Length + _MapiPropertyTerminator.Length);
                    binwriter.Write(encodedId);

                    // Append a 16-bit null terminator at the end to be compatible with the MAPI format.
                    binwriter.Write(_MapiPropertyTerminator);
                }

                stm.Position = 0;
                return stm;
            }
            catch
            {
                Utility.SafeDispose(ref stm);
                throw;
            }
        }

        public void ReplaceOneOffMembers(IEnumerable<Person> members)
        {
            _oneOffs.Clear();

            if (members == null)
            {
                Source.RemoveProperty(_NameEmailKey);
                return;
            }

            var memberList = members as IList<Person> ?? members.ToList();

            using (Stream stm = _GenerateOneOffMembers(memberList))
            {
                if (stm == null)
                {
                    Source.RemoveProperty(_NameEmailKey);
                }
                else
                {
                    Source.SetBinaryProperty(_NameEmailKey, stm, "binary/x-ms-wab-mapi");
                    _oneOffs.AddRange(memberList);
                }
            }
        }

        private Stream _GenerateOneOffMembers(IList<Person> members)
        {
            if (members.Count == 0)
            {
                return null;
            }

            var stm = new MemoryStream();
            try
            {
                var binwriter = new BinaryWriter(stm, _Encoding);

                // The first item is the number of members that are going to be written.
                binwriter.Write(members.Count);

                // CONSIDER: Argument validation: person.Name or Email could have embedded nulls
                // which will really mess up the decoding.

                // Create a string "<name>\0SMTP\0<email>" and encode as unicode.
                foreach (byte[] encodedPerson in members.Select(person => _Encoding.GetBytes(person.Name + "\0SMTP\0" + person.Email)))
                {
                    // We'll write out the length in bytes of the encoded property and then the encoded string into the property.
                    // There's some additional junk at the beginning of the string, and a double-null terminator at the end.
                    binwriter.Write(_OneOffPrefixJunk.Length + encodedPerson.Length + _MapiPropertyTerminator.Length);

                    binwriter.Write(_OneOffPrefixJunk);
                    binwriter.Write(encodedPerson);
                    // Append a 16-bit null terminator at the end to be compatible with the MAPI format.
                    binwriter.Write(_MapiPropertyTerminator);
                }

                stm.Position = 0;
                return stm;
            }
            catch
            {
                Utility.SafeDispose(ref stm);
                throw;
            }
        }
    }
}
